<?php
// 数据导出组件
final class SimpleDataExport {

	static function generateResponseHeader($filename,$charset='UTF-8',$mimeType = 'application/octet-stream'){
		header("Pragma: public");   header("Expires: 0");
		header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
		header("Content-Type: application/force-download");
		header("Content-Type: {$mimeType}; charset={$charset}");
		header("Content-Transfer-Encoding: binary");
		header(self::cDispositionHeader($_SERVER["HTTP_USER_AGENT"],$filename,'attachment',$charset));
	}
	
	static function cDispositionHeader($httpUserAgent,$filename,$contentDisposition='attachment',$charset = 'UTF-8'){
		// 文件名乱码问题
		if (preg_match("/MSIE/", $httpUserAgent)) {
			$filename = urlencode($filename);
			$filename = str_replace("+", "%20", $filename);// 替换空格
			$attachmentHeader = "Content-Disposition: {$contentDisposition}; filename=\"{$filename}\"; charset={$charset}";
		} else if (preg_match("/Firefox/", $httpUserAgent)) {			
			$attachmentHeader = 'Content-Disposition: '.$contentDisposition.'; filename*="utf8\'\'' . $filename. '"' ;
		} else {
			$attachmentHeader = "Content-Disposition: '.$contentDisposition.'; filename=\"{$filename}\"; charset={$charset}";
		}
		
		return $attachmentHeader;
	}
}

// --- Excel 相关

/**
 * 导出 XML格式的 Excel 数据
 */
class XmlExcelExport
{

	/**
	 * 文档头标签
	 * 
	 * @var string
	 */
	private $header = "<?xml version=\"1.0\" encoding=\"%s\"?\>\n<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\" xmlns:x=\"urn:schemas-microsoft-com:office:excel\" xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\" xmlns:html=\"http://www.w3.org/TR/REC-html40\">";

	/**
	 * 文档尾标签
	 * 
	 * @var string
	 */
	private $footer = "</Workbook>";

	/**
	 * 内容编码
	 * @var string
	 */
	private $sEncoding;

	/**
	 * 是否转换特定字段值的类型
	 * 
	 * @var boolean
	 */
	private $bConvertTypes;
	
	/**
	 * 生成的Excel内工作簿的个数
	 * 
	 * @var int
	 */
	private $dWorksheetCount = 0;

	/**
	 * 构造函数
	 * 
	 * 使用类型转换时要确保:页码和邮编号以'0'开头
	 *
	 * @param string $sEncoding 内容编码
	 * @param boolean $bConvertTypes 是否转换特定字段值的类型
	 */
	function __construct($sEncoding = 'UTF-8', $bConvertTypes = false)
	{
		$this->bConvertTypes = $bConvertTypes;
		$this->sEncoding = $sEncoding;
	}

	/**
	 * 返回工作簿标题,最大 字符数为 31
	 *
	 * @param string $title 工作簿标题
	 * @return string
	 */
	function getWorksheetTitle($title = 'Table1')
	{
		$title = preg_replace("/[\\\|:|\/|\?|\*|\[|\]]/", "", empty($title) ? 'Table' . ($this->dWorksheetCount + 1) : $title);
		return substr($title, 0, 31);
	}
	
	/**
	 * 向客户端发送Excel头信息
	 *
	 * @param string $filename 文件名称,无需加 .xls 后缀
	 */
	function generateXMLHeader($filename){
		SimpleDataExport::generateResponseHeader($filename,$this->sEncoding,'application/vnd.ms-excel');		
		echo stripslashes(sprintf($this->header, $this->sEncoding));
	}
	
	/**
	 * 向客户端发送Excel结束标签
	 *
	 * @param string $filename 文件名称,不能是中文
	 */
	function generateXMLFoot(){
		echo $this->footer;
	}
	
	/**
	 * 开启工作簿
	 * 
	 * @param string $title
	 */
	function worksheetStart($title){
		$this->dWorksheetCount ++;
		echo "\n<Worksheet ss:Name=\"" . $this->getWorksheetTitle($title) . "\">\n<Table>\n";
	}
	
	/**
	 * 结束工作簿
	 */
	function worksheetEnd(){
		echo "</Table>\n</Worksheet>\n";
	}
	
	/**
	 * 设置表头信息
	 * 
	 * @param array $header
	 */
	function setTableHeader(array $header){
		echo $this->_parseRow($header);
	}
	
	/**
	 * 设置表内行记录数据
	 * 
	 * @param array $rows 多行记录
	 */
	function setTableRows(array $rows){
		foreach ($rows as $row) echo $this->_parseRow($row);
	}
	
	/**
	 * 将传人的单行记录数组转换成 xml 标签形式
	 *
	 * @param array $array 单行记录数组
	 */
	private function _parseRow(array $row)
	{
		$cells = "";
		foreach ($row as $k => $v){
			$type = 'String';
			if ($this->bConvertTypes === true && is_numeric($v))
				$type = 'Number';
				
			$v = htmlentities($v, ENT_COMPAT, $this->sEncoding);
			$cells .= "<Cell><Data ss:Type=\"$type\">" . $v . "</Data></Cell>\n";
		}
		return "<Row>\n" . $cells . "</Row>\n";
	}

}

/**
 * SimpleExcelExport 提供导出早期 Excel二进制格式的文件的功能
 */
final class SimpleExcelExport {
		
	/**
	 * 输出 xls 头信息
	 */
	static function xlsBOF() {
	    echo pack("ssssss", 0x809, 0x8, 0x0, 0x10, 0x0, 0x0); 
	}
	
	/**
	 * 输出 xls 尾信息
	 */
	static function xlsEOF() {
	    echo pack("ss", 0x0A, 0x00);
	}
	
	static function xlsWriteNumber($Row, $Col, $Value) {
	    echo pack("sssss", 0x203, 14, $Row, $Col, 0x0);
	    echo pack("d", $Value);
	}
	
	static function xlsWriteLabel($Row, $Col, $Value ) {
	    $L = strlen($Value);
	    echo pack("ssssss", 0x204, 8 + $L, $Row, $Col, 0x0, $L);
	    echo $Value;
	}
	
	/**
	 * 导出的Excel 数据需要进行 GBK 编码转换,不然中文会出现乱码
	 *
	 * @param string $value
	 * @return string
	 */
	static function xlsValueTrans($value){
		if (empty($value)) return '';
		return iconv("utf-8","gb2312",$value);
	}
	
	static function generateXlsHeader($filename){
		SimpleDataExport::generateResponseHeader($filename,'utf-8','application/vnd.ms-excel');
	}
	
}

/**
 * 简易 csv 数据导出类
 * 
 */
class SimpleCsvExport {
	
	static function generateXlsHeader($filename){
		SimpleDataExport::generateResponseHeader($filename,'utf-8','text/csv');
		echo(chr(0xEF).chr(0xBB).chr(0xBF));
	}
	
	static function writeRow(array $row){
		echo implode(',',$row) . PHP_EOL;
	}
}

/**
 * 简单的zip 文件生成器
 * 
 * <code>
 * $zip = new SimpleZipExport();
 * $zip->read_dir('static/osp/',true,DOCROOT);
 * // windows 简体中文版 上文件名是 gbk 编码
 * $zip->read_file(sprintf(APPPATH . '/config/%s.doc',iconv('UTF-8','GBK','许消寒')));
 * $zip->read_file(APPPATH . '/config/mvc.php');
 * $zip->download("osp.zip");
 * </code>
 */
class SimpleZipExport  {

	private $zipdata	= '';
	private $directory	= '';
	private $entries	= 0;
	private $file_num	= 0;
	private $offset		= 0;

	/**
	 * Add Directory
	 *
	 * Lets you add a virtual directory into which you can place files.
	 *
	 * @param	mixed	the directory name. Can be string or array
	 * @return	void
	 */
	function add_dir($directory)
	{
		foreach ((array)$directory as $dir)
		{
			if ( ! preg_match("|.+/$|", $dir))
			{
				$dir .= '/';
			}

			$dir_time = $this->_get_mod_time($dir);

			$this->_add_dir($dir, $dir_time['file_mtime'], $dir_time['file_mdate']);
		}
	}

	/**
	 *	Get file/directory modification time
	 *
	 *	@param string	path to file
	 *	@return array	filemtime/filemdate
	 */
	private function _get_mod_time($dir)
	{
		// filemtime() will return false, but it does raise an error.
		$date = (@filemtime($dir)) ? filemtime($dir) : getdate(time());

		$time['file_mtime'] = ($date['hours'] << 11) + ($date['minutes'] << 5) + $date['seconds'] / 2;
		$time['file_mdate'] = (($date['year'] - 1980) << 9) + ($date['mon'] << 5) + $date['mday'];

		return $time;
	}

	/**
	 * Add Directory
	 *
	 * @access	private
	 * @param	string	the directory name
	 * @return	void
	 */
	private function _add_dir($dir, $file_mtime, $file_mdate)
	{
		$dir = str_replace("\\", "/", $dir);

		$this->zipdata .=
			"\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00"
			.pack('v', $file_mtime)
			.pack('v', $file_mdate)
			.pack('V', 0) // crc32
			.pack('V', 0) // compressed filesize
			.pack('V', 0) // uncompressed filesize
			.pack('v', strlen($dir)) // length of pathname
			.pack('v', 0) // extra field length
			.$dir
			// below is "data descriptor" segment
			.pack('V', 0) // crc32
			.pack('V', 0) // compressed filesize
			.pack('V', 0); // uncompressed filesize

		$this->directory .=
			"\x50\x4b\x01\x02\x00\x00\x0a\x00\x00\x00\x00\x00"
			.pack('v', $file_mtime)
			.pack('v', $file_mdate)
			.pack('V',0) // crc32
			.pack('V',0) // compressed filesize
			.pack('V',0) // uncompressed filesize
			.pack('v', strlen($dir)) // length of pathname
			.pack('v', 0) // extra field length
			.pack('v', 0) // file comment length
			.pack('v', 0) // disk number start
			.pack('v', 0) // internal file attributes
			.pack('V', 16) // external file attributes - 'directory' bit set
			.pack('V', $this->offset) // relative offset of local header
			.$dir;

		$this->offset = strlen($this->zipdata);
		$this->entries++;
	}

	/**
	 * Add Data to Zip
	 *
	 * Lets you add files to the archive. If the path is included
	 * in the filename it will be placed within a directory.  Make
	 * sure you use add_dir() first to create the folder.
	 *
	 * @param	mixed
	 * @param	string
	 * @return	void
	 */
	function add_data($filepath, $data = NULL)
	{
		if (is_array($filepath))
		{
			foreach ($filepath as $path => $data)
			{
				$file_data = $this->_get_mod_time($path);

				$this->_add_data($path, $data, $file_data['file_mtime'], $file_data['file_mdate']);
			}
		}
		else
		{
			$file_data = $this->_get_mod_time($filepath);

			$this->_add_data($filepath, $data, $file_data['file_mtime'], $file_data['file_mdate']);
		}
	}

	/**
	 * Add Data to Zip
	 *
	 * @param	string	the file name/path
	 * @param	string	the data to be encoded
	 * @return	void
	 */
	private function _add_data($filepath, $data, $file_mtime, $file_mdate)
	{
		$filepath = str_replace("\\", "/", $filepath);

		$uncompressed_size = strlen($data);
		$crc32  = crc32($data);

		$gzdata = gzcompress($data);
		$gzdata = substr($gzdata, 2, -4);
		$compressed_size = strlen($gzdata);

		$this->zipdata .=
			"\x50\x4b\x03\x04\x14\x00\x00\x00\x08\x00"
			.pack('v', $file_mtime)
			.pack('v', $file_mdate)
			.pack('V', $crc32)
			.pack('V', $compressed_size)
			.pack('V', $uncompressed_size)
			.pack('v', strlen($filepath)) // length of filename
			.pack('v', 0) // extra field length
			.$filepath
			.$gzdata; // "file data" segment

		$this->directory .=
			"\x50\x4b\x01\x02\x00\x00\x14\x00\x00\x00\x08\x00"
			.pack('v', $file_mtime)
			.pack('v', $file_mdate)
			.pack('V', $crc32)
			.pack('V', $compressed_size)
			.pack('V', $uncompressed_size)
			.pack('v', strlen($filepath)) // length of filename
			.pack('v', 0) // extra field length
			.pack('v', 0) // file comment length
			.pack('v', 0) // disk number start
			.pack('v', 0) // internal file attributes
			.pack('V', 32) // external file attributes - 'archive' bit set
			.pack('V', $this->offset) // relative offset of local header
			.$filepath;

		$this->offset = strlen($this->zipdata);
		$this->entries++;
		$this->file_num++;
	}

	/**
	 * Read the contents of a file and add it to the zip
	 *
	 * @return	bool
	 */
	function read_file($path, $preserve_filepath = FALSE)
	{
		if ( ! file_exists($path))
		{
			return FALSE;
		}

		if (FALSE !== ($data = file_get_contents($path)))
		{
			$name = str_replace("\\", "/", $path);

			if ($preserve_filepath === FALSE)
			{
				$name = preg_replace("|.*/(.+)|", "\\1", $name);
			}

			$this->add_data($name, $data);
			return TRUE;
		}
		return FALSE;
	}

	/**
	 * Read a directory and add it to the zip.
	 *
	 * 如果指定$root_path路径,
	 * $path可设为相对路径 路径后面必须带上 "/"
	 *
	 * @param	string	path 目录路径
	 * @param	string	$preserve_filepath 可设置为 true
	 * @param	string	$root_path 基准路径
	 * 
	 * @return	bool
	 */
	function read_dir($path, $preserve_filepath = TRUE, $root_path = NULL)
	{
		if ( ! $fp = @opendir($path))
		{
			return FALSE;
		}

		// Set the original directory root for child dir's to use as relative
		if ($root_path === NULL)
		{
			$root_path = dirname($path).'/';
		}

		while (FALSE !== ($file = readdir($fp)))
		{
			if (substr($file, 0, 1) == '.')
			{
				continue;
			}

			if (@is_dir($path.$file))
			{
				$this->read_dir($path.$file."/", $preserve_filepath, $root_path);
			}
			else
			{
				if (FALSE !== ($data = file_get_contents($path.$file)))
				{
					$name = str_replace("\\", "/", $path);

					if ($preserve_filepath === FALSE)
					{
						$name = str_replace($root_path, '', $name);
					}

					$this->add_data($name.$file, $data);
				}
			}
		}

		return TRUE;
	}

	/**
	 * Get the Zip file
	 *
	 * @return	binary string
	 */
	function get_zip()
	{
		// Is there any data to return?
		if ($this->entries == 0)
		{
			return FALSE;
		}

		$zip_data = $this->zipdata;
		$zip_data .= $this->directory."\x50\x4b\x05\x06\x00\x00\x00\x00";
		$zip_data .= pack('v', $this->entries); // total # of entries "on this disk"
		$zip_data .= pack('v', $this->entries); // total # of entries overall
		$zip_data .= pack('V', strlen($this->directory)); // size of central dir
		$zip_data .= pack('V', strlen($this->zipdata)); // offset to start of central dir
		$zip_data .= "\x00\x00"; // .zip file comment length

		return $zip_data;
	}

	/**
	 * Write File to the specified directory
	 *
	 * Lets you write a file
	 *
	 * @param	string	the file name
	 * @return	bool
	 */
	function archive($filepath)
	{
		if ( ! ($fp = @fopen($filepath, 'wb')))
		{
			return FALSE;
		}

		flock($fp, LOCK_EX);
		fwrite($fp, $this->get_zip());
		flock($fp, LOCK_UN);
		fclose($fp);

		return TRUE;
	}

	/**
	 * Download
	 *
	 * @param	string	the file name
	 * @param	string	the data to be encoded
	 * @return	bool
	 */
	function download($filename = 'backup.zip')
	{
		if ( ! preg_match("|.+?\.zip$|", $filename))
		{
			$filename .= '.zip';
		}

		SimpleDataExport::generateResponseHeader($filename,'utf-8','application/zip');
		$get_zip = $this->get_zip();
		header("Content-Length: %s",strlen($get_zip));
		
		echo $get_zip;
	}

	/**
	 * Initialize Data
	 *
	 * Lets you clear current zip data.  Useful if you need to create
	 * multiple zips with different data.
	 *
	 * @return	void
	 */
	function clear_data()
	{
		$this->zipdata		= '';
		$this->directory	= '';
		$this->entries		= 0;
		$this->file_num		= 0;
		$this->offset		= 0;
	}

}

